package io.httpdoc.objc;

import io.httpdoc.core.*;
import io.httpdoc.core.generation.*;
import io.httpdoc.core.kit.StringKit;
import io.httpdoc.core.modeler.Archetype;
import io.httpdoc.core.modeler.Modeler;
import io.httpdoc.core.strategy.Strategy;
import io.httpdoc.core.strategy.Task;
import io.httpdoc.core.supplier.Supplier;
import io.httpdoc.objc.core.ObjCDocument;
import io.httpdoc.objc.core.ObjCSchema;
import io.httpdoc.objc.external.AFHTTPSessionManager;
import io.httpdoc.objc.external.RSCall;
import io.httpdoc.objc.external.RSClient;
import io.httpdoc.objc.external.RSInvocation;
import io.httpdoc.objc.foundation.*;
import io.httpdoc.objc.fragment.*;
import io.httpdoc.objc.type.ObjCBlockType;
import io.httpdoc.objc.type.ObjCProtocolType;
import io.httpdoc.objc.type.ObjCType;

import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * ObjC 生成器
 *
 * @author 杨昌沛 646742615@qq.com
 * @date 2018-07-25 16:26
 **/
public class ObjCRSNetworkingGenerator implements Generator {
    private final static String DEFAULT_PREFIX = "HD";
    private final String prefix;
    private final Modeler<ObjCFile> modeler;
    private ObjCSELNamingStrategy selNamingStrategy = new ObjCSELDefaultNamingStrategy();

    public ObjCRSNetworkingGenerator() {
        this(DEFAULT_PREFIX);
    }

    public ObjCRSNetworkingGenerator(String prefix) {
        this(prefix, new ObjCMJExtensionModeler());
    }

    public ObjCRSNetworkingGenerator(Modeler<ObjCFile> modeler) {
        this(DEFAULT_PREFIX, modeler);
    }

    public ObjCRSNetworkingGenerator(String prefix, Modeler<ObjCFile> modeler) {
        this.prefix = prefix;
        this.modeler = modeler;
    }

    @Override
    public void generate(Generation generation) throws IOException {
        Document document = generation.getDocument() != null ? new ObjCDocument(prefix, generation.getDocument()) : null;
        if (document == null) return;
        Map<String, Schema> schemas = document.getSchemas() != null ? document.getSchemas() : Collections.<String, Schema>emptyMap();
        Set<Controller> controllers = document.getControllers() != null ? document.getControllers() : Collections.<Controller>emptySet();
        String directory = generation.getDirectory();
        Strategy strategy = generation.getStrategy();
        Collection<ObjCFile> files = new LinkedHashSet<>();
        for (Schema schema : schemas.values()) files.addAll(generate(new SchemaGenerateContext(generation, schema)));
        for (Controller controller : controllers) files.addAll(generate(new ControllerGenerateContext(generation, controller)));
        Collection<Claxx> classes = new LinkedHashSet<>();
        for (ObjCFile file : files) {
            String pkg = file.getPkg();
            String name = file.getName();
            String extension = file.getType().extension;
            String className = pkg + "." + name;
            String classPath = File.separator + className.replace(".", File.separator) + extension;
            Claxx claxx = new Claxx(classPath, file, Preference.DEFAULT);
            classes.add(claxx);
        }
        Task task = new Task(directory, classes);
        strategy.execute(task);
    }

    protected Collection<ObjCFile> generate(SchemaGenerateContext context) {
        Document document = context.getDocument();
        String pkg = context.getPkg();
        boolean pkgForced = context.isPkgForced();
        Supplier supplier = context.getSupplier();
        Schema schema = context.getSchema();
        Archetype archetype = new Archetype(document, pkg, pkgForced, supplier, schema);
        return modeler.design(archetype);
    }

    protected Collection<ObjCFile> generate(ControllerGenerateContext context) {
        Generation generation = context.getGeneration();
        String pkgGenerated = context.getPkg();
        boolean pkgForced = context.isPkgForced();
        Controller controller = context.getController();
        String comment = "Generated By Httpdoc";
        String name = controller.getName();
        String pkgTranslated = controller.getPkg();
        String pkg = pkgForced || pkgTranslated == null ? pkgGenerated : pkgTranslated;

        ObjCClassHeaderFragment interfase = new ObjCClassHeaderFragment();
        interfase.setCommentFragment(new ObjCCommentFragment(controller.getDescription() != null ? controller.getDescription() + "\n" + comment : comment));
        interfase.setName(prefix + name);

        ObjCClassTargetFragment implementation = new ObjCClassTargetFragment();
        implementation.setCommentFragment(new ObjCCommentFragment(controller.getDescription() != null ? controller.getDescription() + "\n" + comment : comment));
        implementation.setName(prefix + name);
        {
            ObjCPropertyFragment client = new ObjCPropertyFragment();
            client.setName("client");
            client.setType(new ObjCProtocolType(ObjCType.valueOf(Cid.class), ObjCType.valueOf(RSClient.class)));
            implementation.addPropertyFragment(client);
        }

        // initWithBaseURL:(NSString *)baseURL
        {
            ObjCResultFragment instancetype = new ObjCResultFragment(ObjCType.valueOf(Cinstancetype.class));
            ObjCSelectorFragment initWithBaseURL = new ObjCSelectorFragment(instancetype, "init");
            ObjCParameterFragment baseURL = new ObjCParameterFragment("baseURL", ObjCType.valueOf(NSString.class));
            initWithBaseURL.addParameterFragment(baseURL);
            //noinspection unchecked
            initWithBaseURL.addSentence("return [self initWithSessionManager:[[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:baseURL]]];", NSURL.class);
            interfase.addSelectorFragment(initWithBaseURL.declaration());
            implementation.addSelectorFragment(initWithBaseURL);
        }

        // initWithSessionManager:(AFHTTPSessionManager *)sessionManager
        {
            ObjCResultFragment instancetype = new ObjCResultFragment(ObjCType.valueOf(Cinstancetype.class));
            ObjCSelectorFragment initWithSessionManager = new ObjCSelectorFragment(instancetype, "init");
            ObjCParameterFragment sessionManager = new ObjCParameterFragment("sessionManager", ObjCType.valueOf(AFHTTPSessionManager.class));
            initWithSessionManager.addParameterFragment(sessionManager);
            initWithSessionManager.addSentence("return [self initWithClient:[[RSAFNetworkingClient alloc] initWithSessionManager:sessionManager]];");
            interfase.addSelectorFragment(initWithSessionManager.declaration());
            implementation.addSelectorFragment(initWithSessionManager);
        }

        // initWithClient:(id<RSClient>)client
        {
            ObjCResultFragment instancetype = new ObjCResultFragment(ObjCType.valueOf(Cinstancetype.class));
            ObjCSelectorFragment initWithClient = new ObjCSelectorFragment(instancetype, "init");
            ObjCParameterFragment client = new ObjCParameterFragment("client", new ObjCProtocolType(ObjCType.valueOf(Cid.class), ObjCType.valueOf(RSClient.class)));
            initWithClient.addParameterFragment(client);
            initWithClient.addSentence("self = [super init];");
            initWithClient.addSentence("if (self) {");
            initWithClient.addSentence("    _client = client;");
            initWithClient.addSentence("}");
            initWithClient.addSentence("return self;");
            interfase.addSelectorFragment(initWithClient.declaration());
            implementation.addSelectorFragment(initWithClient);
        }

        List<Operation> operations = controller.getOperations() != null ? controller.getOperations() : Collections.<Operation>emptyList();
        for (Operation operation : operations) {
            Collection<ObjCSelectorFragment> selectors = generate(new OperationGenerateContext(generation, controller, operation));
            if (selectors == null) continue;
            for (ObjCSelectorFragment selector : selectors) {
                interfase.addSelectorFragment(selector.declaration());
                implementation.addSelectorFragment(selector);
            }
        }

        return Arrays.asList(
                new ObjCFile(pkg, interfase.getName(), ObjCFile.Type.INTERFACE, interfase),
                new ObjCFile(pkg, implementation.getName(), ObjCFile.Type.IMPLEMENTATION, implementation)
        );
    }

    protected Collection<ObjCSelectorFragment> generate(OperationGenerateContext context) {
        Operation operation = context.getOperation();
        ObjCSelectorFragment selector = new ObjCSelectorFragment();
        selector.setSelNamingStrategy(selNamingStrategy);
        selector.setComment(operation.getDescription());
        Result result = operation.getResult();
        String comment = result != null ? result.getDescription() : null;
        ObjCResultFragment call = new ObjCResultFragment(new ObjCProtocolType(ObjCType.valueOf(Cid.class), ObjCType.valueOf(RSCall.class)), "RSCall");
        selector.setResultFragment(call);
        selector.setName(operation.getName());
        Generation generation = context.getGeneration();
        Controller controller = context.getController();
        List<Parameter> parameters = operation.getParameters() != null ? operation.getParameters() : Collections.<Parameter>emptyList();
        Collection<ObjCParameterFragment> fragments = generate(new ParameterGenerateContext(generation, controller, operation, parameters));
        selector.getParameterFragments().addAll(fragments);

        Supplier supplier = context.getSupplier();
        ObjCType type = result != null && result.getType() != null
                ? result.getType().isVoid()
                ? null
                : result.getType().isPrimitive()
                ? ((ObjCSchema) result.getType().toWrapper()).toObjCType(supplier)
                : ((ObjCSchema) result.getType()).toObjCType(supplier)
                : null;
        ObjCType returnType = type != null ? type : ObjCType.valueOf(Cid.class);

        Map<String, ObjCType> map = new LinkedHashMap<>();
        map.put("success", ObjCType.valueOf(Cbool.class));
        map.put("result", returnType);
        map.put("error", ObjCType.valueOf(NSError.class));
        ObjCBlockType callback = new ObjCBlockType(map);
        ObjCParameterFragment success = new ObjCParameterFragment();
        success.setType(callback);
        success.setName("callback");
        success.setVariable("callback");
        success.setComment("(success, " + (StringKit.isEmpty(comment) ? "result" : comment) + ", error)");
        selector.getParameterFragments().add(success);

        StringBuilder builder = new StringBuilder();
        List<String> segments = Arrays.asList(controller.getPath(), operation.getPath());
        for (String segment : segments) {
            if (segment == null) continue;
            builder.append("/").append(segment);
        }
        String uri = builder.toString().replaceAll("/+", "/");
        String method = operation.getMethod();

        selector.addSentence("RSInvocation *invocation = [RSInvocation " + method.toUpperCase() + ":@\"" + uri + "\"];", RSInvocation.class);
        int index = 0;
        for (ObjCParameterFragment fragment : fragments) {
            Parameter parameter = parameters.get(index++);
            String variable = parameter.getType().isPrimitive() ? "@(" + fragment.getVariable() + ")" : fragment.getVariable();
            String name = "@\"" + (parameter.getName() != null ? parameter.getName() : "") + "\"";
            String path = "@\"" + (parameter.getPath() != null ? parameter.getPath() : "") + "\"";
            switch (parameter.getScope()) {
                case Parameter.HTTP_PARAM_SCOPE_HEADER:
                    selector.addSentence("[invocation addHeaderValue:" + variable + " ofName:" + name + "];");
                    break;
                case Parameter.HTTP_PARAM_SCOPE_PATH:
                    selector.addSentence("[invocation addPathValue:" + variable + " ofName:" + name + "];");
                    break;
                case Parameter.HTTP_PARAM_SCOPE_MATRIX:
                    selector.addSentence("[invocation addMatrixValue:" + variable + " ofName:" + name + " forPath:" + path + "];");
                    break;
                case Parameter.HTTP_PARAM_SCOPE_QUERY:
                    selector.addSentence("[invocation addQueryValue:" + variable + " ofName:" + name + "];");
                    break;
                case Parameter.HTTP_PARAM_SCOPE_BODY:
                    selector.addSentence("[invocation addBodyValue:" + variable + " ofName:" + name + "];");
                    break;
                case Parameter.HTTP_PARAM_SCOPE_COOKIE:
                    selector.addSentence("[invocation addCookieValue:" + variable + " ofName:" + name + "];");
                    break;
                case Parameter.HTTP_PARAM_SCOPE_FIELD:
                    selector.addSentence("[invocation addQueryValue:" + variable + " ofName:" + name + "];");
                    break;
            }
        }
        selector.addSentence("invocation.resultType = NSClassFromString(@\"" + returnType.getName() + "\");");

        selector.addSentence("return [_client invoke:invocation callback:callback];");

        return Collections.singleton(selector);
    }

    protected Collection<ObjCParameterFragment> generate(ParameterGenerateContext context) {
        Supplier supplier = context.getSupplier();
        List<Parameter> parameters = context.getParameters();
        Collection<ObjCParameterFragment> fragments = new LinkedHashSet<>();
        for (int i = 0; parameters != null && i < parameters.size(); i++) {
            Parameter parameter = parameters.get(i);
            ObjCParameterFragment fragment = new ObjCParameterFragment();
            String name = parameter.getName();
            name = StringKit.isBlank(name) ? parameter.getType().toName() : name;
            String alias = parameter.getAlias();
            String variable = StringKit.isBlank(alias) ? name : alias;
            loop:
            while (true) {
                for (ObjCParameterFragment prev : fragments) {
                    if (variable.equals(prev.getVariable())) {
                        variable = String.format("_%s", variable);
                        continue loop;
                    }
                }
                break;
            }
            fragment.setName(name);
            fragment.setComment(parameter.getDescription());
            fragment.setType(((ObjCSchema) parameter.getType()).toObjCType(supplier));
            fragment.setVariable(variable);
            fragments.add(fragment);
        }
        return fragments;
    }

    public ObjCSELNamingStrategy getSelNamingStrategy() {
        return selNamingStrategy;
    }

    public void setSelNamingStrategy(ObjCSELNamingStrategy selNamingStrategy) {
        this.selNamingStrategy = selNamingStrategy;
    }
}
